using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO;
using System.Net;

namespace SharepointListMigrator
{
    /// <summary>
    /// Does all the generic Migrating work and uses the strategy pattern to perform list specific update steps.
    /// In debug mode this file writes the following data to disc (in the bin folder). These files are overwritten on each iteration.
    /// Source Items Xml - <Generic List Name>-SourceListItems.xml
    /// Destination Update Command Xml - <Generic List Name>-UpdateListItemsInput.xml
    /// Destination Update Results Xml - <Generic List Name>-UpdateListItemsResults.xml
    /// </summary>
    public class ListMigrator
    {

        private NetworkCredential credentials=null;
        private IListMigratorAlgorithm migratorAlgorithm = null; //uses the Strategy pattern to encapsulate specific list instructions

        public string ListName
        {
            get { return migratorAlgorithm.GenericListName; }
        }

        public ListMigrator(IListMigratorAlgorithm migratorAlgorithm, string username, string password, string domain)
        {
            if (migratorAlgorithm == null)
                throw new ArgumentNullException("The parameter 'migratorAlgorithm' cannot be null.");

            this.migratorAlgorithm = migratorAlgorithm; 
            credentials = WebServiceHelper.CreateCredentialToken(username, password, domain); //this validates the parameters too
        }

        public void MigrateList(string sourceSiteUrl, string destinationSiteUrl)
        {
            XmlNode items = GetSourceListItems(sourceSiteUrl);

            //get the rs:data node
            items = items.FirstChild;
            while(true) //uses true so that when we exit we know that it was because we found the item, not because we may have found the item or it may not have been found
            {
                if (items == null)
                    throw new ApplicationException("The node 'rs:data' cannot be found in the source list item xml.");

                if (items.Name == "rs:data")
                    break;
                else
                    items = items.NextSibling;
            }

            XmlNode nodeNewItemDetails = AddItemsToDestinationList(items, destinationSiteUrl);
            if (migratorAlgorithm.MoveAttachments)
            {
                List<IdMatch> idMatches = BuildIdMatches(items, nodeNewItemDetails);
                CopyAttachments(idMatches, sourceSiteUrl, destinationSiteUrl);
            }
        }

        private XmlNode GetSourceListItems(string sourceSiteUrl)
        {
            XmlNode items = null;
            //-- get items from the source list --
            using (Source.Lists.Lists sourceService = WebServiceHelper.GetSourceWebService(sourceSiteUrl, credentials))
            {
                XmlNode nodeViewAllFields = new System.Xml.XmlDocument().CreateNode(XmlNodeType.Element, "ViewFields", ""); //we want all fields, so dont specify any
                items = sourceService.GetListItems(migratorAlgorithm.SourceListName, migratorAlgorithm.SourceViewGuid, null, nodeViewAllFields, migratorAlgorithm.NumberOfItemsToMigrate.ToString(), null);
            }

#if DEBUG
            //log results
            using (StreamWriter writer = new StreamWriter(string.Format("{0}-SourceListItems.xml", migratorAlgorithm.GenericListName)))
                writer.Write(items.OuterXml);
#endif
            return items;
        }

        private XmlNode AddItemsToDestinationList(XmlNode items, string destinationSiteUrl)
        {
            XmlElement xmlWrapper = WebServiceHelper.CreateUpdateListItemXMLWrapper(migratorAlgorithm.DestinationViewGuid);
            StringBuilder strb = new StringBuilder();
            int counter = 1;

            //add the item commands
            foreach (XmlNode child in items.ChildNodes)
            {
                if (child.OuterXml.Trim().Length > 0)
                {
                    strb.Append(string.Format("<Method ID='{0}' Cmd='New'>",counter.ToString()) + migratorAlgorithm.BuildItemUpdateXml(child) + "</Method>");
                    counter++;
                }
            }

            xmlWrapper.InnerXml = strb.ToString();

#if DEBUG
            //log input xml
            using (StreamWriter writer = new StreamWriter(string.Format("{0}-UpdateListItemsInput.xml", migratorAlgorithm.GenericListName)))
                writer.Write(xmlWrapper.OuterXml);
#endif

            //-- write to destination list --
            XmlNode results = null;
            using (Destination.Lists.Lists destinationService = WebServiceHelper.GetDestinationWebService(destinationSiteUrl, credentials))
            {
                results = destinationService.UpdateListItems(migratorAlgorithm.DestinationListName, xmlWrapper);

#if DEBUG
                //log results
                using (StreamWriter writer = new StreamWriter(string.Format("{0}-UpdateListItemsResults.xml", migratorAlgorithm.GenericListName)))
                    writer.Write(results.OuterXml);
#endif
            }
            //look for errors
            XmlNode errorsSearch = results.FirstChild;
            while (errorsSearch != null)
            {
                if (errorsSearch.FirstChild.Name == "ErrorCode")
                    if (errorsSearch.FirstChild.InnerText != "0x00000000")
                    {
                        if (errorsSearch.FirstChild.NextSibling.Name == "ErrorText")
                            throw new ApplicationException(string.Format("Error with update on row '{0}': {1}", errorsSearch.Attributes["ID"].Value.Replace(",New", string.Empty), errorsSearch.FirstChild.NextSibling.InnerText));
                        else
                            throw new ApplicationException("Error with update: " + errorsSearch.OuterXml);
                    }

                errorsSearch = errorsSearch.NextSibling;
            }

            return results;
        }

        /// <summary>
        /// Builds a list of matches from source items to the new destination items, so that we can attach the attachments to the correct new items later
        /// </summary>
        private List<IdMatch> BuildIdMatches(XmlNode items, XmlNode updateResults)
        {
            List<IdMatch> idMatches = new List<IdMatch>();
            //we walk the list of child nodes, ignoring the blank records
            //but we only step thru the updateResults when we do not have a blank record. This is why we need the counter variable
            updateResults = updateResults.FirstChild;
            foreach (XmlNode child in items.ChildNodes)
            {
                if (child.OuterXml.Trim().Length > 0)
                {
                    if (updateResults == null)
                        throw new ApplicationException("The updateResults collection ended before we had finished walking thru the item child nodes.");

                    IdMatch match = new IdMatch();
                    match.SourceId = int.Parse(child.Attributes["ows_ID"].Value);

                    //get the z:row child node so we can get the details
                    XmlNode rowNode = updateResults.FirstChild;
                    while (true) //use this so we know that we exit when we find it without having to do another check outside the loop
                    {
                        if (rowNode == null)
                            throw new ApplicationException(string.Format("The updateResults node '{0}' did not have a 'z:row' child node.",
                                updateResults.OuterXml));
                        if (rowNode.Name == "z:row")
                            break;

                        rowNode = rowNode.NextSibling;
                    }

                    //get the destinationId
                    //we expect that the updateResults will be ordered the same as the items nodes collection
                    if (rowNode.Attributes["ows_Title"].Value == child.Attributes["ows_Title"].Value) //check the title, so we know we have a match
                    {
                        match.DestinationId = int.Parse(rowNode.Attributes["ows_ID"].Value);

                        idMatches.Add(match);
                        updateResults = updateResults.NextSibling;

                    }
                    else
                        throw new ApplicationException(string.Format("We expected the next updateResults item to have the title '{0}' but it was '{1}'",
                            child.Attributes["ows_Title"].Value,
                            updateResults.Attributes["ows_Title"].Value));
                }
            }

            return idMatches;
        }

        private void CopyAttachments(List<IdMatch> idMatches,string sourceSiteUrl, string destinationSiteUrl)
        {
            XmlNode attachments = null;
            using (System.Net.WebClient webClient = new System.Net.WebClient())
            {
                webClient.Credentials = credentials;
                foreach (IdMatch match in idMatches)
                {
                    //get the attachments
                    attachments = GetSourceListItemAttachments(sourceSiteUrl, match.SourceId);

                    //Add them
                    foreach (XmlNode attachment in attachments.ChildNodes)
                    {
                        string fileName = attachment.InnerXml.Substring(attachment.InnerXml.LastIndexOf(@"/") + 1);
                        byte[] data = webClient.DownloadData(attachment.InnerXml);
                        AddAttachment(destinationSiteUrl, match.DestinationId, fileName, data);
                    }
                }
            }
        }

        private XmlNode GetSourceListItemAttachments(string sourceSiteUrl, int itemId)
        {
            XmlNode items = null;
            //-- get items from the source list --
            using (Source.Lists.Lists sourceService = WebServiceHelper.GetSourceWebService(sourceSiteUrl, credentials))
            {
                XmlNode nodeViewAllFields = new System.Xml.XmlDocument().CreateNode(XmlNodeType.Element, "ViewFields", ""); //we want all fields, so dont specify any
                items = sourceService.GetAttachmentCollection(migratorAlgorithm.SourceListName, itemId.ToString());
            }
            return items;

        }

        private void AddAttachment(string destinationSiteUrl, int itemId, string fileName, byte[] fileContents)
        {
            using (Destination.Lists.Lists destinationService = WebServiceHelper.GetDestinationWebService(destinationSiteUrl, credentials))
            {
                //doesnt return anything useful
                destinationService.AddAttachment(migratorAlgorithm.DestinationListName, itemId.ToString(), fileName, fileContents);
            }
        }

       
    }
}
